Skip to content

feat: AST SQL rewriting - #10604

Open
MazterQyou wants to merge 1 commit into
masterfrom
feat-ast-sql-rewriting
Open

MazterQyou wants to merge 1 commit into
masterfrom
feat-ast-sql-rewriting

Conversation

@MazterQyou

@MazterQyou MazterQyou commented Apr 1, 2026

Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

GET ?query= → extract filters from logical plan, no rewriting:
{ "status": "ok", "filters": [ ... ] }

POST { query, add | set | delete | replace } - exactly one op required, else Exactly one of add, set, delete or replace parameters is required:

  • add: [filters]: adds each; already-present identical filter = no-op.
  • set: [filters]: drops the filter predicates of the outermost WHERE + HAVING, then adds the set. Predicates that are not Cube filters (join conditions, subquery predicates, predicates over computed columns) are kept. set: [] strips all outermost filters. CTEs and subqueries untouched.
  • delete: [filters]: attempts deletion; all occurrences of equal filters removed; not-present = tolerated no-op.
  • replace: { old: [filters], new: [filters] }: exact set→set; every old must be found (else error); all occurrences replaced. Single old + single new in same clause → in-place, positions preserved; otherwise remove-all + add.

Behaviour change to rest4sql (/v1/convert-query)

rest4sql and the new /v1/sql-filters endpoints share parse_security_context_arg. A security context that is present but not a JSON string now throws (Security context must be a JSON string / Security context is not valid JSON), where rest4sql previously planned the query as an unauthenticated call, silently dropping the filters derived from the context. In-repo callers go through @cubejs-backend/native's JS wrappers, which always pass a JSON string or null, and are unaffected; anything calling the native functions directly must do the same.

@github-actions github-actions Bot added the rust Pull requests that update Rust code label Apr 1, 2026
@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.39%. Comparing base (42bfe75) to head (d6d4ea2).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##           master   #10604       +/-   ##
===========================================
+ Coverage   61.23%   85.39%   +24.15%     
===========================================
  Files         247      262       +15     
  Lines       19824    92185    +72361     
  Branches     4043        0     -4043     
===========================================
+ Hits        12140    78717    +66577     
- Misses       7112    13468     +6356     
+ Partials      572        0      -572     
Flag Coverage Δ
cube-backend ?
cubesql 85.39% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

});
}));

app.get(`${this.basePath}/v1/sql-filters`, userMiddlewares, userAsyncHandler(async (req: any, res) => {
});
}));

app.post(`${this.basePath}/v1/sql-filters`, jsonParser, userMiddlewares, userAsyncHandler(async (req, res) => {
@MazterQyou
MazterQyou marked this pull request as ready for review August 5, 2026 16:42
@MazterQyou
MazterQyou requested review from a team as code owners August 5, 2026 16:42
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 4m 1s —— View job


Review complete — 1 high, 3 medium, 4 low. Main concern: unvalidated user values emitted as raw SQL numeric literals (#3722392675).

Full review — findings, praise, and checklist

Findings

# Sev Where Issue
1 High ast_conv.rs:440 value_to_expr_by_member_type wraps caller-supplied strings in ast::Value::Number, which sqlparser prints verbatim. For number-typed dimensions / numeric measures, equals/notEquals skip the single_numeric_value validation that gt/lt do apply, so values: ["0 OR 1=1"] renders as = 0 OR 1=1. No test covers a numeric member.
2 Medium gateway.ts:1586 const { old } = replace runs before the replace === null guard → replace: null throws TypeError and surfaces as 500 instead of the intended 400 UserError.
3 Medium ast_conv.rs:1300 delete is documented as a tolerated no-op when the filter is absent, but modify_sql_ast returns Err when the member is unresolvable — so deleting a filter on a member not projected in the outermost SELECT errors instead of no-op'ing. Breaks idempotent callers.
4 Medium ast_conv.rs:1203 Re-parses + re-prints the whole (growing) SQL string per filter → O(n²) with n full parser runs, and no cap on array length anywhere from the HTTP body down. Same shape in delete_sql_filters and the replace fallback. Combined with the unrated-limited routes CodeQL flagged, that's a cheap CPU-pin.
5 Low gateway.ts:1507 Native errors come back as HTTP 200 + {status:"error"}. Matches sql4sql's precedent, but the endpoint now has three failure shapes (200 in-band / 400 / 500).
6 Low ast_conv.rs:709 alias_for_relation_in_from matches on the last name part only, ignoring schema and the WITH list → a CTE named after a cube is misresolved as CubeTable. Caught later by the verification re-plan, but as an opaque planner error.
7 Low ast_conv.rs:1085 let _ = plan.accept(&mut visitor) swallows visitor errors, so a truncated filter set becomes the verification oracle — misleading "was not applied" errors, and a false success in delete_sql_filters (inverted check).
8 Low ast_conv.rs:419 LIKE %/_ escaping relies on the implicit backslash escape with escape_char: None; explicit is dialect-independent.

Non-blocking, no inline comment posted:

  • Docs. Two new public REST endpoints with no /docs-mintlify page. The PR checklist has the docs box unticked — worth confirming whether these are intended as public API or internal-only.
  • Tests. Rust coverage of the AST layer is good (~19 tests: CTEs, derived tables, filter groups, duplicates, in-place replace, set-operation rejection). What's untested: the validation branches in modifySqlFilters (the four Array.isArray guards, the exactly-one-op rule, the replace shape check), the sql-server.ts/js/index.ts pass-through, and any non-string member type. The four gateway guards are exactly where finding 2 lives.
  • filter_key treats values: null and values: [] as identical and a missing member as "", so {} and {member: "", operator: ""} collide. Harmless given the surrounding validation, but the canonicalization is looser than the "perfect match" wording suggests.

What's good

  • Clean separation: pure AST manipulation in ast_conv.rs, Neon glue in sql_filters.rs, HTTP in gateway.ts. Each layer is testable on its own, and the Rust tests exercise the AST layer directly without a session.
  • The verify-by-replanning pattern in add_filters_and_verify is the right instinct — asserting the rewrite actually landed in the logical plan rather than trusting the string edit.
  • spawn_sql_filters_task correctly factors out the promise/deferred dance across all five entry points, with the try_settle_with failure logged rather than silently dropped, and the auth-context construction matches the existing rest4sql/sql4sql precedent exactly.
  • Doc comments on the tricky predicates (member_output_column_in_query, apply_action_to_outermost_select) state the invariants precisely, including what deliberately doesn't qualify.
  • assertApiScope('sql', …) reuses the existing scope rather than inventing a new one; route middleware ordering matches every other POST in the file.

Checklist

  • Gather context (diff, changed files)
  • Review ast_conv.rs (core rewriting logic)
  • Review native bridge (sql_filters.rs, node_export.rs, js/index.ts)
  • Review gateway HTTP endpoints (security, validation)
  • Assess test coverage
  • Post review

No prior threads of mine on this PR, so nothing to resolve or dedupe against. Findings are from static reading — I did not build the Rust crate or run the test suite.

• branch [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

@MazterQyou
MazterQyou force-pushed the feat-ast-sql-rewriting branch from f6f3c15 to 7965b9f Compare August 5, 2026 16:45
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 4m 9s —— View job


Review complete — solid design, but 2 high / 4 medium / 5 low open; nothing from the previous round was addressed in this push.

Full review — expand for findings, prior-round status, and notes
  • Gather context (diff, changed files)
  • Review ast_conv.rs (core rewriting logic)
  • Review native bridge (sql_filters.rs, node_export.rs, js/index.ts)
  • Review gateway HTTP endpoints (security, validation)
  • Assess test coverage
  • Check prior review threads / post review

New this round (4 inline comments posted)

Sev Where Issue
High ast_conv.rs:944-952 add_expr_to_clause AND-appends to an unparenthesized top-level OR clause. sqlparser's Display is not precedence-aware, so WHERE a OR b + filter c renders as a OR b AND c → reparses as a OR (b AND c). Silently changes query semantics, and add_filters_and_verify cannot detect it (the requested filter is in the plan). This file already wraps its own OR combinations in Expr::Nested at lines 364 / 405 / 694 for exactly this reason — the pre-existing clause just doesn't get the same treatment. No test starts from an unparenthesized OR.
Medium ast_conv.rs:956-968 clause_contains_expr and remove_exprs don't descend through Expr::Nested. WHERE (a AND b) breaks the "already-present = no-op" guarantee for add (duplicate predicate returned as success) and makes delete/replace fail on filters that are plainly present.
Medium ast_conv.rs:1219-1228 Verification oracle is plan-wide but the rewrite is outermost-SELECT-only. extract_filters_from_plan flattens filters from CTEs/subqueries too, so an identical filter inside a CTE can satisfy the add check for a filter never applied — and conversely makes delete of a CTE-only filter always error, contradicting the documented tolerant no-op. applied is discarded at line 1206.
Low ast_conv.rs:883 is_direct_member_ref matches bare Identifier on name alone; two joined cubes sharing a dimension name resolve non-deterministically.

Previous round — all 8 threads re-verified, all still apply

Re-affirmed rather than re-posted (unresolved state already signals this):

  • Highast_conv.rs:456 value_to_expr_by_member_type emits caller-supplied strings as raw Value::Number for number-typed members. equals/notEquals skip the parse::<f64>() validation that gt/lt do via single_numeric_value. values: ["0 OR 1=1"] renders straight into the WHERE clause.
  • Mediumgateway.ts:1591 replace destructured before the null guard → replace: null throws TypeError and surfaces as 500 instead of 400.
  • Mediumast_conv.rs:1085 let _ = plan.accept(...) swallows visitor errors; a truncated filter set is then used as the verification oracle.
  • Mediumast_conv.rs:1209 per-filter re-parse/re-print is O(n²) with no cap on array length, on routes CodeQL already flags as unrated-limited.
  • Low — ast_conv.rs:1306 delete errors instead of no-op'ing when the member isn't resolvable in the outermost SELECT.
  • Low — ast_conv.rs:712 a CTE named after a cube shadows it and is misresolved as MemberSource::CubeTable.
  • Low — ast_conv.rs:436 %/_ escaped with \ but escape_char: None, relying on the dialect's default.
  • Low — gateway.ts:1507 native errors returned as HTTP 200 + in-band {status:"error"}, giving the endpoint three distinct failure shapes (200/400/500).

The two github-advanced-security rate-limiting comments are consistent with every other /v1/* route on this gateway — not a regression introduced here, though the unbounded-array concern above makes these two routes more attractive as a CPU sink than most.

What's good

  • Member resolution is deliberately conservative — member_output_column_in_query refuses post-processed and generated columns, and CTEs/subqueries are never rewritten. The docstrings explaining why (lines 571-575, 784-787) are genuinely helpful.
  • Round-tripping the rewritten SQL back through convert_sql_to_cube_query and diffing the extracted filter set is a strong correctness net, and it catches most member-resolution mistakes even where the AST logic is imprecise.
  • assertApiScope('sql', …) on both routes, and securityContext threaded through to transport.meta() — authorization matches the existing /v1/sql surface.
  • Test coverage is broad for a change this size: all four ops, filter groups, nested groups, mixed WHERE/HAVING rejection, CTE non-modification, derived tables, set-operation rejection, duplicate filters. Gaps are the specific shapes called out above (unparenthesized OR, parenthesized AND, numeric-typed members with equals).

Not verified

  • Did not build or run the Rust test suite (cargo test -p cubesql) or yarn lint — the sqlparser Display claim underpinning the high-severity finding rests on the vendored fork's behavior, which I inferred from this file's own Nested-wrapping at lines 364/405/694 rather than reading the fork's source (the cargo git checkout is outside the accessible working directory). Worth a one-line unit test to confirm.
  • The PR checklist has docs unchecked, and /v1/sql-filters is a new public REST surface with no page under docs-mintlify/. Per the root CLAUDE.md these belong in /docs-mintlify (not /docs).
• [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
@MazterQyou
MazterQyou force-pushed the feat-ast-sql-rewriting branch from 7965b9f to ea52be1 Compare August 5, 2026 18:16
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 5m 25s —— View job


Review complete — every finding from the last two rounds is addressed. 4 new: 1 medium, 2 low, 1 test-coverage.

Full review — expand for findings, prior-round status, and notes
  • Gather context (diff, changed files)
  • Check prior review threads (dedupe / resolve stale)
  • Review ast_conv.rs (core rewriting logic)
  • Review native bridge (sql_filters.rs, node_export.rs, js/index.ts)
  • Review gateway HTTP endpoints
  • Assess test coverage
  • Post review

Previous rounds — 12 threads resolved

All 12 of my open threads are fixed in this push, most with a test pinning the behaviour:

Prior finding Resolution
High — raw Value::Number from caller input (equals skipped gt's validation) numeric_value_expr + is_numeric_literal (rejects empty, inf/NaN, stray chars) now gate every numeric literal, value_to_expr_by_member_type included. test_modify_sql_ast_numeric_value_validation
HighAND-appending to an unparenthesized top-level OR add_expr_to_clause wraps Or/Xor clauses in Expr::Nested. test_modify_sql_ast_or_clause_is_parenthesized
Mediumreplace destructured before the null guard → 500 Guard moved ahead of the destructure (gateway.ts:1610)
MediumNested not descended in clause_contains_expr / remove_exprs Both recurse now; remove_exprs re-wraps and collapses correctly. test_modify_sql_ast_parenthesized_clause
Mediumlet _ = plan.accept(…) swallowed visitor errors Propagates; extract_filters_from_plan returns Result
Medium — O(n²) re-parse per filter, unbounded array modify_sql_ast_many parses/prints once for the whole batch; MAX_SQL_FILTERS = 100 in the gateway
Medium — plan-wide oracle vs outermost-only rewrite applied is now honoured in delete_sql_filters, and the doc comments state the plan-wide caveat explicitly (see one residual case below)
Low — delete errored instead of no-op'ing on an unresolvable member Remove goes through resolve_filter_exprOk(false). test_delete_sql_filters_unresolvable_member
Low — CTE named after a cube misresolved as CubeTable cte_names threaded through the resolver. test_modify_sql_ast_cte_shadowing_cube_name
Low — %/_ escaped with \ but escape_char: None Answered with a comment: an explicit ESCAPE takes a rewrite path that yields no Cube filter, so the implicit backslash is deliberate. Reasonable
Low — native errors as HTTP 200 + in-band {status:"error"} resSqlFilters maps status === 'error' to 400
Low — bare Identifier matched on name alone allow_unqualified requires the cube to be the sole relation. test_modify_sql_ast_unqualified_ref_in_join

New this round (4 inline comments)

Sev Where Issue
Medium ast_conv.rs:857 CTE name matching is asymmetric — shadow detection lowercases via cte_names, the CTE lookup compares byte-for-byte. WITH Orders AS (…) SELECT … FROM orders (valid Postgres, both fold to orders) resolves in neither branch, so the endpoint rejects it with is not available in the outermost SELECT. A refusal, not a wrong rewrite — but avoidable.
Low ast_conv.rs:1486 The applied gate closes "filter only in a CTE"; the mirror case is open. A filter present in both the outermost SELECT and a CTE gets applied == true, the CTE copy keeps it in new_keys, and delete errors was not deleted on a rewrite that did exactly what was asked — discarding the correct SQL.
Low sql_filters.rs:69 parse_filters_arg's cx.throw_error rejects with a plain JS Error, so a malformed filter item ("member": 42) is a 500 with a raw serde message, while every other bad input on this endpoint is a 400 UserError. Same for replace's two arrays.
Test ast_conv.rs:3053 Three shapes reach the verification oracle untested: measure/HAVING filters (only asserted at the AST level — the likeliest branch to fail verification, and the only one synthesizing a function from agg_type), inDateRange (normalize_filter_value exists because the planner reshapes these values, but nothing tests that the reshaped values still filter_key-match), and any value containing ' (the last remaining literal-injection surface; safety rests entirely on the vendored fork's Display).

Minor, no thread posted: the schema-qualification nit from the CTE thread is still live — alias_for_relation_in_table_factor matches on the last name part, so FROM analytics.Orders is treated as cube Orders. And filter_key still canonicalizes values: null and values: [] identically with a missing member as "", so {} and {member: "", operator: ""} collide; harmless given the surrounding validation.

Nested and/or groups aren't counted toward MAX_SQL_FILTERS, but serde_json's 128-deep parse limit in parse_filters_arg bounds recursion depth before resolve_filter_expr/canonical_filter_json ever recurse, and breadth is linear against a single parse — no report.

What's good

  • The fixes are real fixes, not papered-over ones: modify_sql_ast_many restructures the batch path rather than just capping input, and add_expr_to_clause's comment states the precedence invariant it's protecting. Each one landed with a test that would catch a regression.
  • numeric_value_expr centralizes the literal-emission rule in one place with a docstring explaining why parse::<f64>() alone is insufficient — exactly the shape that keeps this from re-breaking when a new operator is added.
  • resolve_filter_expr vs require_filter_expr is a clean way to express "absent is fine for delete, an error for add" without duplicating the resolution logic, and it's what makes the tolerant-delete contract actually hold.
  • Doc comments now carry the awkward caveats rather than hiding them (add_sql_filters on plan-wide verification, member_output_column_in_query on what deliberately doesn't qualify, the like_family_expr note on why ESCAPE is omitted).
  • resSqlFilters gives the endpoint one failure shape, and the guard ordering in modifySqlFilters is now uniform across all four ops.

Not verified

  • Did not build or run cargo test -p cubesql or yarn lint — findings are from static reading. The sqlparser Display behaviour underpinning the '-escaping note is unverified (the cargo git checkout is outside the accessible working directory), which is part of why a test pinning it is worth having.
  • Docs are still unchecked in the PR checklist, and /v1/sql-filters (GET + POST) is a new public REST surface with no page under docs-mintlify/. Per the root CLAUDE.md these belong in /docs-mintlify, not /docs — worth confirming whether these endpoints are intended as public API or internal-only.
• [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread docs-mintlify/reference/core-data-apis/rest-api/reference.mdx Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
@claude

claude Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 8m 39s —— View job


No blocking issues — 3 low (docs), 2 prior threads re-affirmed

Review details
  • Read changed files
  • Review gateway / sql-server (JS side)
  • Review native bridge (sql_filters.rs, auth.rs, rest4sql.rs)
  • Review ast_conv.rs (non-test code read in full; tests sampled)
  • Review tests & docs
  • Resolve stale threads / dedupe
  • Post findings

Posted inline (3, all documentation)

# Site Concern
1 docs-mintlify/reference/core-data-apis/rest-api/reference.mdx:347 "Only the outermost SELECT is read and rewritten" — reading is plan-wide (extract_filters_from_plan walks every CubeScanNode, CTEs and subqueries included), so GET reports filters delete/replace cannot touch
2 reference.mdx:416 Docs cover the "filter may be kept" case but not the widening one: remove_reported_column_predicates drops every literal predicate on the column when the reported filter is alone on its member, so delete/set can remove predicates the caller never named
3 reference.mdx:398 500-filter cap is documented; the 4 MiB MAX_SQL_BYTES cap, which produces a user-facing 400, is not

Review threads

Listing returned 6 of my own threads (2 further threads are github-advanced-security CodeQL, left alone). Resolved 4 as fixed by this round:

  • tokio worker parked by thread::scope + .expect panic → rewrite_sql now spawns a named thread and awaits a oneshot; spawn failure is CubeError::internal, and the thread::scope variant is #[cfg(test)]-only
  • gateway.ts comment claiming /v1/load parity on values → now states null is refused
  • explicit-stack filter normalization → replaced with recursive normalizeFilter
  • docs claiming numbers/booleans are written into SQL as strings → now says the SQL follows the member's type

Re-affirmed 2 (still open, not re-posted):

  • auth.rs:97rest4sql behaviour change: an unparseable security context used to be silently dropped and now throws. Fail-closed is the right call, but no test touches either error string, and the PR description doesn't mention the change to an existing endpoint.
  • doc-comment density in ast_conv.rs — the three named sites were trimmed, but 34 doc-comment blocks of 4+ lines (11 of 5+) remain; the thread's ask was a pass over the file.

What holds up

  • Injection surface: string values go through Value::SingleQuotedString (quote-doubling on render), numerics through is_numeric_literal — a character whitelist plus a finite f64 parse, so inf/NaN/whitespace/; are all refused (ast_conv.rs:250). Identifiers come from the data model, quoted. Nothing user-controlled reaches the SQL text unescaped.
  • Error classification: in_band_or_thrown (sql_filters.rs:169) routes CubeErrorCauseType::Internal to a thrown JS error (500) and everything else in-band (400), and plan_and_extract_filters keeps a planner fault internal while a query that won't plan stays the caller's. Both directions are covered by sql-filters.test.ts.
  • Resource bounds: MAX_SQL_BYTES, MAX_FILTERS, MAX_CLAUSE_PREDICATES, MAX_RELATION_DEPTH/MAX_RELATION_EXPANSIONS, MAX_EXPR_NODES — and the walks that protect the stack are themselves iterative, which is the part that's usually missed. set additionally bounds the filters the plan reports, not just the request's.
  • Verification: additions are checked against the plan of the rewritten query, removals against the AST (with the reason — plan-wide extraction would false-alarm on a CTE copy — written down). verify_additions plans the original only on failure, to tell a query that never planned from a rewrite that broke it.
  • Auth: both routes assertApiScope('sql', ...) before anything reaches native, and the test asserts zero native calls on a 403.

Not verified

  • No test run. node_modules is absent and rust/cubesql/target is empty in this checkout, so neither cubejs-api-gateway's Jest suite nor cargo test -p cubesql was executed — a from-scratch cubesql build isn't feasible inside the review budget. The PR's own CI (Build, Build native, Drivers tests) was still in_progress at review time. Findings above are from reading, not from a failing run.
  • CodeQL's two "missing rate limiting" alerts on the new routes match the pattern of every other gateway route (/v1/sql, /v1/dry-run, …); not raised as a finding.
· branch `feat-ast-sql-rewriting`

Comment thread docs-mintlify/reference/core-data-apis/rest-api/reference.mdx Outdated
Comment thread docs-mintlify/reference/core-data-apis/rest-api/reference.mdx Outdated
Comment thread docs-mintlify/reference/core-data-apis/rest-api/reference.mdx
@claude

claude Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 9m 37s —— View job


1 high, 1 low, 1 re-affirmed — 2 inline comments posted; 3 stale threads resolved.

Review of PR #10604 — AST SQL rewriting
  • Read changed files
  • Review Rust (ast_conv.rs, sql_filters.rs, native bindings)
  • Review TS (gateway, sql-server, index.ts) + tests + docs
  • Handle prior review threads
  • Post findings

Findings

High — rust/cubesql/cubesql/src/compile/ast_conv.rs:644 (thread)
MAX_SQL_BYTES (4 MiB) is justified by the rewrite thread's 256 MiB stack, but plan_and_extract_filters (ast_conv.rs:2706) runs convert_sql_to_cube_query inside the with_session future — on a tokio worker's default 2 MiB stack. By the file's own measurement (REWRITE_STACK_BYTES: "20k left-deep conjuncts overflow 2 MiB"), a ~700 KB query overflows the worker stack while parsing/dropping the sqlparser AST, which is SIGSEGV and takes the whole Node process with it. GET reaches it with no rewrite at all; set/delete/replace reach it on the original SQL. ClauseBudget doesn't help — it counts only the outermost clause, so the chain can sit in a CTE. The two stack tests (test_clause_at_the_bound_renders_on_a_worker_stack, test_long_chain_outside_the_outermost_select_renders) both exercise modify_sql_ast_many and never plan.

Low — packages/cubejs-api-gateway/src/gateway.ts:1588 (thread)
status is documented as always present, but only resSqlFilters's own in-band 400 carries it. Scope failures, query parameter must be a non-empty string, Exactly one of add, set…, add parameter must be an array of filters and thrown native errors all go through handleError, whose bodies have no status.

Not posted

Comment policy — several explanatory comments run past the 3-line house rule (ast_conv.rs:68, :698, :708, :722, :1357, :1606, :1637, :1753) and the SqlFiltersUpdate field docs (:2673) re-spell already-typed names. Skipped as a duplicate of PRRT_kwDOCOH19M6jSMa2, which already asks for the same pass over the same file.

CodeQL rate-limiting (gateway.ts:488, :496) — the same pattern as every other route registered in initApp; not new to this PR.

Checked and clean

  • Injection. Filter values reach the AST as Value::SingleQuotedString / Value::Boolean / validated numerics; is_numeric_literal (ast_conv.rs:266) rejects inf, NaN and whitespace before a number is rendered verbatim, and LIKE patterns escape \, %, _. Identifiers come from the meta model via MetaMember::get_from_ctx, not from the request.
  • Auth. Both routes assert the sql scope before anything reaches the native layer, matching sql4sql. parse_security_context_arg (auth.rs:97) tightens rest4sql, which previously swallowed a malformed context and planned as if the caller had none.
  • Date semantics. date_range_upper pads a bare upper bound to T23:59:59.999, matching BaseFilter.formatToDate; beforeOrOnDate emits <= '2024-01-31' with no padding, which matches allocateTimestampParams taking the from-formatted param for that operator.
  • Recursion bounds. Group nesting is capped by serde_json's own 128-deep limit (and/or are Vec<serde_json::Value>); filter_members, clause_predicate_count, is_planning_time_date and filter_column_key are all iterative with node budgets.
  • Conjunct splitting. and_conjuncts_* (borrowing) and into_and_conjuncts_* (consuming) walk in the same order, and with_keys re-keys rather than mispairing if they ever diverge. append_expr_to_clause parenthesizes an existing top-level OR/XOR before AND-ing, which sqlparser's non-precedence-aware rendering needs.
  • Tests. 66 Rust tests in ast_conv.rs covering CTE chains, derived tables, case mismatches, group flattening, duplicate filters, in-place replacement and the filter-count bound; the gateway tests cover scope, op-count, shape validation, value coercion and the in-band-vs-thrown split.

Not run

yarn dependencies are not installed in this checkout, so jest and cargo test could not be run here. CI (Build, Build native, Drivers tests) was still in progress at review time.

Review threads

The listing returned 5 threads of mine (plus 2 CodeQL threads, left alone). Resolved 3 — the reference.mdx read-vs-rewrite conflation, the undocumented column-fallback widening, and the undocumented SQL-size cap, all three now covered by the docs text. Re-affirmed 2: auth.rs:97 (the rest4sql behaviour change still wants a note in the PR description) and the ast_conv.rs comment-length pass.

· branch `feat-ast-sql-rewriting`

Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts
@claude

claude Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 6m 22s —— View job


Review complete — 1 high, 1 medium; 4 prior threads resolved

Details
  • Read the diff (14 files, +8648 / -17)
  • Review ast_conv.rs — filter extraction, AST rewriting, bounds
  • Review native bindings (sql_filters.rs, auth.rs, rest4sql.rs, js/index.ts)
  • Review API gateway endpoints + sql-filters.test.ts
  • Review docs (rest-api/reference.mdx)
  • Reconcile prior review threads
  • Post findings

Findings posted inline

Sev Where Finding
High rust/cubesql/cubesql/src/compile/ast_conv.rs:686 count_sql_operators treats any ' outside a string literal as opening one — an apostrophe in a -- comment, a /* */ comment or a double-quoted identifier zeroes the rest of the count, bypassing MAX_SQL_OPERATORS. A 700 KB, 20 000-conjunct GET /v1/sql-filters?query=… then plans past this file's own measured overflow point (10 000 conjuncts) on the 256 MiB rewrite stack. A Rust stack overflow is SIGSEGV, so it aborts the Node process.
Medium rust/cubesql/cubesql/src/compile/ast_conv.rs:716 Unbounded per-request thread spawns with 256 MiB stacks that planning actually touches — ~50 MiB of committed stack per in-flight request at the operator ceiling, 2–3 threads per set/delete/replace, no concurrency cap and no rate limiting on either route.

Review threads

The listing returned 6 unresolved threads, 4 of them mine. All 4 resolved — each concern is addressed in the current diff, and each got a reply saying so rather than silent closure:

Thread Verdict
auth.rs:97rest4sql behaviour change undocumented, direct-native case untested Fixed: PR description now has a Behaviour change to rest4sql section; test/sql.test.ts covers rest4sql / getSqlFilters / addSqlFilters called directly
ast_conv.rs (outdated) — explanatory comments over the 3-line house rule Fixed: MAX_CLAUSE_PREDICATES, with_keys, normalized_filter_json all tightened; no long narrative blocks left in the non-test region
ast_conv.rs:642 — planning ran on a 2 MiB tokio worker stack Fixed: plan_and_extract_filters now spawns rewrite_thread() and block_ons inside it, pinned by test_planning_runs_off_the_worker_stack. The bound introduced alongside that fix is what the new high finding is about — different root cause, own thread
gateway.ts:1588status documented as always present but absent on gateway-raised errors Fixed: docs row now qualifies it

The two remaining unresolved threads are CodeQL's (missing rate limiting on gateway.ts:488 / :496) — not mine to resolve, and the medium finding above is adjacent to them.

What holds up well

  • Filter values reaching SQL are not injectable: members must resolve against MetaContext, numeric literals go through is_numeric_literal before being rendered verbatim, strings go through SingleQuotedString, and LIKE patterns escape \, %, _ with no ESCAPE clause.
  • The error split is right. in_band_or_thrown (sql_filters.rs:137) keeps caller mistakes as { status: 'error' } → 400 and lets genuine internal failures reach the gateway's handler as 500s, and parse_filters_arg deliberately answers malformed JSON in-band rather than throwing.
  • verify_additions plans only the rewritten query and falls back to planning the original just to tell "never planned" from "the rewrite broke it" — the right cost/diagnosis trade-off.
  • The Rust test module is genuinely thorough: ~60 cases covering CTE shadowing, identifier-case mismatches, unqualified refs over joins, group flattening, in-place replacement, date round-tripping, and the stack bounds.
  • Gateway coercion of numbers/booleans in values matches /v1/load, and the docs call out the null divergence.

Not verified

cargo and python3 are outside this run's sandbox, so I could not build cubesql, run cargo test -p cubesql ast_conv, or execute a scratch reproduction of the operator counter. Both findings are from reading the code paths end to end; the high one traces a specific input through count_sql_operators line by line, but the resulting abort was not observed.

· branch [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 6m 49s —— View job


Reviewed — 1 medium-high, 1 medium, 2 low; no correctness bugs found in the rewrite logic

Review details
  • Read changed files (gateway, sql-server, native bindings, ast_conv.rs)
  • Analyze correctness / security / error handling
  • Check test coverage and docs
  • Handle prior review threads
  • Post findings

Findings posted inline

# Severity Where Issue
1 medium-high ast_conv.rs:625, :2783 rewrite_permit()'s permit lives in the async fn's state, not in the spawned closure. Dropping the caller's future releases the permit while the detached 256 MiB-stack thread keeps running, so a client that aborts requests can exceed the concurrency cap without bound.
2 medium ast_conv.rs:2783 The permit is held across convert_sql_to_cube_query, i.e. across Node transport round-trips. A cap sized for stack/CPU (available_parallelism()) is applied to an I/O-bound wait; set/replace take three permits in sequence, so a few slow planners block every /v1/sql-filters call including GET.
3 low reference.mdx:401 Docs say 500 filters per request, but replace_sql_filters charges assert_filter_count separately on old (:3016) and new (:3017), allowing 1000.
4 low test/sql-filters.test.ts Tests call the protected methods directly; the HTTP→args mapping at gateway.ts:489 and :497-503 is never executed and is not type-checked either.

What I checked and found sound

  • SQL injection. All literal paths go through ast::Value::SingleQuotedString (sqlparser doubles the quote on render) or numeric_value_expr, which requires is_numeric_literal — a character whitelist and a finite f64 parse, so inf/NaN/whitespace/0x are all refused rather than pasted verbatim. like_family_expr escapes \, % and _ and deliberately omits ESCAPE, matching what the filter rewrite rules assume. Identifier quoting in expr_key handles the doubled-quote form.
  • Recursion / stack. The walks that could be driven by attacker input (clause_predicate_count, filter_column_key, into_and_conjuncts_with, is_planning_time_date, count_sql_operators) are all iterative with explicit node budgets, and the recursive ones (normalized_filter_json, count_filter_json_leaves, filter_members) sit behind serde_json's own 128-level deserialization limit. assert_sql_size runs on the worker before anything parses.
  • No semaphore deadlock. set/replace acquire permits strictly in sequence, never two at once — worth stating since it's the obvious hazard given finding 2.
  • Auth. Both routes assert the sql scope before the native call, and the test asserts zero native calls on a 403. The parse_security_context_arg change is the right direction: the old rest4sql code swallowed a malformed context and planned the query unauthenticated, silently dropping context-derived filters, which was a genuine security bug. The behaviour change is called out in the PR body and the new sql.test.ts cases cover all three rejection paths against the raw native functions.
  • Error classification. in_band_or_thrown keying on CubeErrorCauseType::Internal, with CompilationError::Internal mapped to CubeError::internal at the plan site, cleanly separates 400 from 500; the gateway tests assert both.
  • verify_additions planning the original only on failure, purely to distinguish "never planned" from "the rewrite broke it", is a nice touch.

yarn test and cargo check were both unavailable in this environment — no node_modules in the checkout, and the cargo check invocation was denied by the sandbox — so nothing here is backed by a run. All four findings are static.

Review threads: the listing returned 2 of my own threads across both pages; I resolved both — the hand-rolled in_literal scanner in count_sql_operators (now lexing via sqlparser::tokenizer::Tokenizer) and the unbounded rewrite threads (now behind rewrite_permits()). 0 re-affirmed. The two remaining unresolved threads are github-advanced-security CodeQL rate-limiting findings on gateway.ts:488/:496, which I left alone; the new routes are no less rate-limited than the existing /v1/sql and /v1/load next to them.

• [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs Outdated
Comment thread docs-mintlify/reference/core-data-apis/rest-api/reference.mdx
Comment thread packages/cubejs-api-gateway/test/sql-filters.test.ts
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 8m 21s —— View job


No blocking issues — 1 medium, 4 low, posted inline

Full review
  • Gather context (diff, PR body, changed files)
  • Review rust/cubesql/.../ast_conv.rs (filter extraction + rewriting core)
  • Review packages/cubejs-backend-native (Rust bindings + JS wrappers)
  • Review packages/cubejs-api-gateway (endpoints, validation, tests)
  • Review docs
  • Check prior review threads / resolve stale
  • Post inline findings + final summary

Findings

# Severity Where What
1 Medium ast_conv.rs:1895 ClauseKeys::forget drops the whole cached key set on every removal, so set/delete re-render the entire clause up to 5× per reported filter; MAX_FILTERS × MAX_CLAUSE_PREDICATES = ~2.5×10⁷ Expr::to_string() calls for one authenticated POST
2 Low gateway.ts:1681 normalizeFilter returns a non-object entry unchanged, so {"add":["oops"]} clears gateway validation and surfaces the native serde message (invalid type: string "oops", expected struct V1LoadRequestQueryFilterItem) instead of a written one
3 Low gateway.ts:1667 4-line comment whose last two sentences restate normalizeValue line for line
4 Low gateway.ts:1671 normalizeValue / normalizeFilter / assertFilterArray are pure but declared inside the handler, 35 of the method's 90 lines
5 Low gateway.ts:1707 Four near-identical dispatch blocks, resSqlFilters called four times

What I checked and found sound

  • SQL injection through values. Every leaf path funnels into ast::Value::SingleQuotedString / Value::Number / Ident::with_quote, numeric values are gated by is_numeric_literal (ast_conv.rs:264 — rejects inf/NaN/whitespace/1-2 via the char check and is_finite), and LIKE patterns escape \, %, _ before quoting. test_modify_sql_ast_string_value_quoting (ast_conv.rs:5490) drives O'Brien' OR 1=1 -- through equals and contains and re-parses the output to assert it is still a single predicate. Good test to have.
  • Recursion depth on attacker-controlled filter JSON. resolve_filter_expr, normalized_filter_json and count_filter_json_leaves all recurse over group nesting, but and/or are Vec<serde_json::Value> and serde_json caps deserialization at 128 levels, so depth is bounded before the Rust code sees it. The JS-side normalizeFilter recursion is unbounded but its RangeError is caught by the surrounding try/handleError.
  • Unbounded walks. Every AST walk is iterative with an explicit budget (MAX_EXPR_NODES, MAX_RELATION_EXPANSIONS, MAX_RELATION_DEPTH, MAX_EXPR_NESTING) rather than recursive — including the self-referencing-CTE case.
  • append_expr_to_clause precedence. sqlparser rendering is not precedence-aware, and the code correctly parenthesizes an existing top-level OR/XOR clause before AND-ing (ast_conv.rs:1650). Covered by test_modify_sql_ast_or_clause_is_parenthesized.
  • Error classification. in_band_or_thrown (sql_filters.rs:137) routes CubeErrorCauseType::Internal to a thrown JS error (→ 500) and everything else in-band (→ 400), and plan_and_extract_filters maps CompilationError::Internal the same way. verify_additions re-plans the original query only on failure, to tell "never planned" from "the rewrite broke it" — the right trade, since planning is the expensive part.
  • parse_security_context_arg behaviour change. The stricter contract is the correct one: the old rest4sql code swallowed a malformed context with .ok() and planned the query unauthenticated, silently dropping the security-context filters. packages/cubejs-backend-native/test/sql.test.ts:876 calls the raw native functions directly (not the JS wrappers) to pin it, which is exactly the surface that changed.
  • Response serialization. V1LoadRequestQueryFilterItem carries skip_serializing_if = "Option::is_none" on every field, so the response matches the clean objects in the docs and exact_filter_key's to_string is order-stable.
  • Docs. reference.mdx is accurate against the code, including the awkward parts — the column-match fallback that "may drop predicates that were not asked about", set leaving a CTE-only filter in place, the 500-filter bound counting group leaves, and null in values being refused unlike /v1/load. The page is already in docs.json, so no nav change needed.
  • Gateway test coverage. test/sql-filters.test.ts covers scope, the exactly-one-op rule, per-op dispatch, empty query, non-array filters, number/boolean/null in values, replace shape, in-band error → 400, thrown error → 500, and the express route wiring for both verbs.

Not raised

  • CodeQL's two missing-rate-limiting alerts on gateway.ts:488/496 are already on the PR, and the new routes are wired exactly like the /v1/load, /v1/sql and /v1/convert-query routes beside them — rate limiting is absent repo-wide for these, so it is not this PR's to fix.
  • The 4-line doc comments in ast_conv.rs (e.g. lines 1068, 2733, 2844) carry real non-obvious rationale rather than restating the code, so they earn the extra line.

Review threads

The listing returned 4 threads of mine; I resolved all 4 and re-affirmed none:

  • PRRT_kwDOCOH19M6jp63O, PRRT_kwDOCOH19M6jp7Cu — the rewrite semaphore and the spawned-thread machinery are gone; rewrite_sql (ast_conv.rs:606) now calls modify_parsed_query inline and there is no Semaphore left under rust/cubesql/cubesql/src.
  • PRRT_kwDOCOH19M6jp7KTreplace now charges the 500-filter bound once across both lists: assert_filter_count(old_filters.iter().chain(new_filters))? at ast_conv.rs:2862.
  • PRRT_kwDOCOH19M6jp7Uj — route wiring is now tested: the sql filters routes block at test/sql-filters.test.ts:273 drives supertest against a real express() with gateway.initApp(app).

The two remaining unresolved threads are github-advanced-security's and were left untouched.

Not verified

I could not run anything in this environment — yarn jest test/sql-filters.test.ts in packages/cubejs-api-gateway and cargo test in rust/cubesql were both denied by the sandbox, so the 54 Rust tests and the 13 gateway tests are unexecuted on my side. Finding 1's ~2.5×10⁷ figure is derived from the constants and the call sites, not measured.

· branch [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread packages/cubejs-api-gateway/src/gateway.ts Outdated
Comment thread rust/cubesql/cubesql/src/compile/ast_conv.rs
Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
@claude

claude Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MazterQyou's task in 7m 32s —— View job


No correctness or security defects found — 4 low-severity comments posted inline (0 high, 0 medium, 4 low)

Review details
  • Read changed files
  • Review Rust (ast_conv.rs, sql_filters.rs, auth.rs)
  • Review JS (gateway.ts, sql-server.ts, index.ts)
  • Review tests + docs
  • Check prior review threads
  • Post findings

Inline comments

File Concern
packages/cubejs-api-gateway/src/gateway.ts:1718 result === undefined used as the "so it must be replace" sentinel; suggestion replaces it with a .find() over the op names
rust/cubesql/cubesql/src/compile/ast_conv.rs:596 modify_sql_ast_many is a cfg(test) alias forwarding to modify_parsed_query with identical arguments
rust/cubesql/cubesql/src/compile/mod.rs:4 Module name ast_conv doesn't describe the SQL-filter API it holds; 7508 lines in one file is worth splitting now rather than later
packages/cubejs-backend-native/src/auth.rs:97 5-line doc comment where one sentence carries it; representative of the comment density across the new Rust

What I checked and found sound

SQL injection. Every value that reaches the rendered SQL goes through a validating constructor: numerics through is_numeric_literal (ast_conv.rs:264, which rejects inf/NaN/whitespace that bare f64::parse would admit), booleans through parse::<bool>, strings through ast::Value::SingleQuotedString, whose Display in sqlparser 0.62 doubles embedded quotes. LIKE patterns escape \, %, _ and deliberately omit ESCAPE (ast_conv.rs:437-444) because backslash is already the Postgres default — correct, and the comment explains why an ESCAPE clause would break the rewrite path.

The rest4sql behaviour change is a fix, not just a change. The old code (rest4sql.rs:131-135) did .parse::<serde_json::Value>().ok() and downcast(...).unwrap_or(None) — a security context that failed to parse was silently swallowed and the query planned unauthenticated, dropping exactly the filters that context existed to add. parse_security_context_arg now throws on both. The direct-native tests at sql.test.ts:876-906 cover all three refusal paths.

Error classification. in_band_or_thrown (sql_filters.rs:137) routes CubeErrorCauseType::Internal to a thrown JS error (→ 500 through handleError) and everything else to { status: "error" } (→ 400 through resSqlFilters), and plan_and_extract_filters maps CompilationError::Internal to CubeError::internal to feed it. verify_additions (ast_conv.rs:2818) plans the original query only on failure, so a rewrite that broke a query is told apart from a query that never planned — and it plans the rewritten query once, not both, which is the right side to spend on.

Unbounded work. Each walk is capped: MAX_FILTERS (500, counted in group leaves, assert_filter_count), MAX_CLAUSE_PREDICATES (10 000, charged incrementally by ClauseBudget rather than recounted per action), MAX_RELATION_DEPTH/MAX_RELATION_EXPANSIONS (which also stops a self-referencing CTE), MAX_EXPR_NODES. The recursive helpers that aren't explicitly capped (count_filter_json_leaves, normalized_filter_json, resolve_filter_expr) are bounded by serde_json's own 128-deep deserialization limit, since the filters arrive as a JSON string.

Cache invalidation in ClauseKeys. Keyed on (ClauseKind, normalize_dates) while ignore_qualifier is constant for the batch (the outermost FROM doesn't change) and sole_reported isn't read by MatchContext::key — so the key isn't under-specified. forget is called on every path that mutates a clause other than by append: the removal batch (remove_filters_from_select:980, once per clause for the whole batch, not per filter) and both Replace branches. note_appended updates every set of the clause, so no set goes stale.

Split ordering. into_and_conjuncts_with and and_conjuncts_with push right-then-left onto the same stack, so both walk left-to-right and with_keys can zip them — and it re-keys defensively rather than mispairing if the lengths ever diverge (ast_conv.rs:2065).

Parenthesization on append. append_expr_to_clause wraps an existing top-level OR/XOR in Expr::Nested before AND-ing, since sqlparser's rendering isn't precedence-aware. On the other side, every multi-part expression this module builds (date_range_expr, multi_value_join, the group combiner in resolve_filter_expr) already wraps itself when it has more than one part.

Behaviours that look surprising but are intended and documented: set removing predicates by column when a reported filter isn't spelled the way this API writes it, set leaving a CTE-only reported filter in place, and delete of an absent filter being a no-op. All three are called out in reference.mdx, including the "compare the returned filters with what was asked for" warning.

Tests. sql-filters.test.ts covers the gateway's own checks (scope, exactly-one-op, query validation, filter shape at depth, number/boolean coercion, null rejection, in-band 400 vs thrown 500) with a SQLServer subclass rather than an any cast, and separately exercises the express routes so the request→argument mapping is tested rather than assumed. The Rust side carries ~4400 lines of unit tests in the same file.

CodeQL. The two "missing rate limiting" alerts on the new routes match the existing /v1/convert-query and other authorized routes in this gateway — no new exposure, no action needed here.

Review threads

The listing returned 5 of my own threads, all from earlier rounds. All 5 resolved — each fix is in the working tree: comment trimmed onto assertSqlFilterArray, non-object filter entries rejected in normalizeSqlFilter, helpers hoisted to module scope, singleListOps lookup table with a single resSqlFilters call, and removals batched with clause_keys.forget called once per clause per batch. None withdrawn, so no replies were needed. 0 re-affirmed. The 2 remaining unresolved threads are github-advanced-security's CodeQL alerts and were left untouched.

· [`feat-ast-sql-rewriting`](https://github.com/cube-js/cube/tree/feat-ast-sql-rewriting)

Comment on lines +1718 to +1738
let result: SqlFiltersResponse | undefined;

for (const name of Object.keys(singleListOps) as (keyof typeof singleListOps)[]) {
if (operands[name] !== undefined) {
result = await singleListOps[name](assertSqlFilterArray(operands[name], name));
}
}

if (result === undefined) {
if (typeof replace !== 'object' || replace === null || Array.isArray(replace)) {
throw new UserError('replace parameter must be an object with old and new filter arrays');
}

const { old: oldFilters, new: newFilters } = replace as Record<string, unknown>;
result = await this.sqlServer.replaceSqlFilters(
query,
assertSqlFilterArray(oldFilters, 'replace.old'),
assertSqlFilterArray(newFilters, 'replace.new'),
context.securityContext,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result === undefined is load-bearing here as "no single-list op ran, so it must be replace", but nothing in the type says a handler can't resolve to undefined — the guarantee comes from the count check 4 lines up. The loop also iterates all three names when at most one can be set.

Naming the op once makes both facts local:

Suggested change
let result: SqlFiltersResponse | undefined;
for (const name of Object.keys(singleListOps) as (keyof typeof singleListOps)[]) {
if (operands[name] !== undefined) {
result = await singleListOps[name](assertSqlFilterArray(operands[name], name));
}
}
if (result === undefined) {
if (typeof replace !== 'object' || replace === null || Array.isArray(replace)) {
throw new UserError('replace parameter must be an object with old and new filter arrays');
}
const { old: oldFilters, new: newFilters } = replace as Record<string, unknown>;
result = await this.sqlServer.replaceSqlFilters(
query,
assertSqlFilterArray(oldFilters, 'replace.old'),
assertSqlFilterArray(newFilters, 'replace.new'),
context.securityContext,
);
}
const name = (Object.keys(singleListOps) as (keyof typeof singleListOps)[])
.find((op) => operands[op] !== undefined);
let result: SqlFiltersResponse;
if (name) {
result = await singleListOps[name](assertSqlFilterArray(operands[name], name));
} else {
if (typeof replace !== 'object' || replace === null || Array.isArray(replace)) {
throw new UserError('replace parameter must be an object with old and new filter arrays');
}
const { old: oldFilters, new: newFilters } = replace as Record<string, unknown>;
result = await this.sqlServer.replaceSqlFilters(
query,
assertSqlFilterArray(oldFilters, 'replace.old'),
assertSqlFilterArray(newFilters, 'replace.new'),
context.securityContext,
);
}

Comment on lines +595 to +603
#[cfg(test)]
fn modify_sql_ast_many(
sql: &str,
actions: &[ModifyAction],
ctx: &MetaContext,
reported: &ReportedFilters,
) -> DFResult<(String, Vec<bool>)> {
modify_parsed_query(sql, actions, ctx, reported)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modify_sql_ast_many forwards to modify_parsed_query with the identical argument list and return type — it adds a second name for the same function that only exists under cfg(test). Have the tests call modify_parsed_query and drop this.

Comment on lines +97 to +101
/// Reads a security context passed from JS as a JSON string. A missing, null
/// or undefined argument is an unauthenticated call; one that is present and
/// is not a JSON string is a bug on the JS side and is thrown rather than
/// planned as if the caller had no context, which would drop the filters
/// derived from it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Five lines to say one thing. The load-bearing sentence is the last clause — everything before it restates the match below it:

Suggested change
/// Reads a security context passed from JS as a JSON string. A missing, null
/// or undefined argument is an unauthenticated call; one that is present and
/// is not a JSON string is a bug on the JS side and is thrown rather than
/// planned as if the caller had no context, which would drop the filters
/// derived from it.
/// Reads a security context passed from JS as a JSON string; missing/null is
/// an unauthenticated call. A non-string throws rather than planning as if
/// there were no context, which would silently drop its filters.

Same shape elsewhere in the new Rust: ast_conv.rs:939-943, ast_conv.rs:1196-1200, ast_conv.rs:2320-2323, sql_filters.rs:133-136. Not worth a comment each, but the file-wide density is high enough that the comments that do carry a non-obvious invariant (ast_conv.rs:1773-1775 on why the clause is taken rather than borrowed, ast_conv.rs:180-183 on notInDateRange only matching NOT BETWEEN) don't stand out from the ones that narrate.

use self::engine::CubeContext;

pub mod ast_conv;
pub mod builder;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ast_conv reads as a generic AST-conversion utility, but the module is the whole /v1/sql-filters implementation: filter→SQL expression rendering, member resolution through CTEs and derived tables, conjunct matching, and the four public rewrite entry points. Something like sql_filters would say what it is, and would let a reader looking for this feature find it from the module list.

7508 lines in one file (≈3000 code, ≈4400 tests) is also a lot to carry as a unit — the filter→ast::Expr rendering (ModifyAction), the member resolution (resolve_member_source and friends), and the clause matching (expr_key/ClauseKeys/*_conjuncts*) are three separable concerns with no cycles between them. Worth splitting into a sql_filters/ directory before it grows further; splitting a file this size later is a much worse diff to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Issues that require a documentation improvement javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants